import enum
import types
import typing
import datetime
import dataclasses

from enum import Enum
from decimal import Decimal
from typing import ForwardRef  # type: ignore
from collections import OrderedDict

from typing import (
    Any,
    Dict,
    List,
    Type,
    Union,
    Tuple,
    Optional
)

from dataclasses import (
    Field,
    MISSING,
    is_dataclass
)


class Empty: ...


class UnknowFieldType(Exception):
    ...


# 基础类型映射
TYPE_MAP = {
    ...: "any",

    str: "string",

    bool: "boolean",

    int: "number",
    float: "number",

    list: "Array<any>",
    tuple: "Array<any>",

    dict: "{ [key:string] : any }",

    Any: "any",
    Empty: "void",
    Decimal: "string",
    datetime.date: "string",
    datetime.datetime: "string",

    None: "null",
    types.NoneType: "null"
}

# python 基础值类型 映射 ts值类型
VALUE_CONVERT_MAP = {
    int: lambda x: str(x),
    str: lambda x: f"'{x}'",
    float: lambda x: str(x),
    bool: lambda x: str(x).lower(),
    Enum: lambda x: f"{x.__class__.__name__}.{x.name}",
    types.NoneType: lambda x: "null",
}


def is_type(obj: Any):
    """
    检测对象是值还是类型
    """
    try:
        return issubclass(type(obj), type)
    except TypeError:
        return False


def is_interfice(obj: type) -> bool:
    """
    检测是否可以转成interfice
    """
    try:
        if typing.get_type_hints(obj):
            return True
        return False
    except TypeError:
        return False


def is_field_required(field: Field) -> bool:
    """
    是否为必需字段
    """
    return field.default is MISSING


def check_is_subclass(obj: type, cls: type) -> Optional[bool]:
    """
    判断obj是否为cls的子类
    """
    try:
        return issubclass(obj, cls)
    except TypeError:
        return None


def enum_to_typescript(enum_class: Type[Enum]) -> str:
    """
    枚举类型转换
    """
    body = ",\n".join(f"  {member.name} = {python_value_to_typescript(member.value)}" for member in enum_class)
    return "export enum {} {{\n{}\n}}".format(enum_class.__name__, body)


def dataclass_to_typescript(schema: Any) -> str:
    """
    dateclass类型转换
    """
    interface_body = "\n".join(
        [
            f"  {field.name}: {field_to_typescript(field)}"
            if is_field_required(field)
            else f"  {field.name}?: {field_to_typescript(field)}"
            for field in dataclasses.fields(schema)
        ]
    )
    return "export interface {} {{\n{}\n}}".format(schema.__name__, interface_body)


def py_type_hints_to_typescript_interfice(obj: type) -> str:
    """
    将一个有类型标注的类转换为interfice
    """
    interface_body = "\n".join([
        f"  {name}: {python_type_to_typescript(field)};"
        for name, field in typing.get_type_hints(obj).items()
    ])

    return "export interface {} {{\n{}\n}}".format(obj.__name__, interface_body)


class TypeNode:
    """
    类型节点
    """

    def __init__(self, schema) -> None:
        if is_dataclass(schema):
            self.is_enum = False
            self.is_dataclass = True
            self.is_interfice = False

        elif check_is_subclass(schema, Enum):
            self.is_enum = True
            self.is_dataclass = False
            self.is_interfice = False

        elif is_interfice(schema):
            self.is_enum = False
            self.is_interfice = True
            self.is_dataclass = False

        else:
            raise Exception(f"{schema} is neither Enum or dataclass or type_hints")

        self.schema = schema

    @property
    def path(self) -> str:
        """
        返回类型的路径
        """
        return f"{self.schema.__module__}.{self.schema.__name__}"

    def to_typescript(self) -> str:
        """
        转换为typescript
        """
        if self.is_dataclass:
            return dataclass_to_typescript(self.schema)

        if self.is_interfice:
            return py_type_hints_to_typescript_interfice(self.schema)

        return enum_to_typescript(self.schema)

    def get_dependencies(self) -> List["TypeNode"]:
        """
        扫描依赖类型
        """
        deps: List[TypeNode] = []
        if self.is_dataclass:
            for field in dataclasses.fields(self.schema):
                deps += get_field_dependencies(field.type)

        elif self.is_interfice:
            for field in typing.get_type_hints(self.schema).values():
                deps += get_field_dependencies(field)

        else:
            # Naively handle Enum dependencies
            deps = []

        return deps

    def __repr__(self) -> str:
        return self.to_typescript()

    @staticmethod
    def chk_type_node(obj: type) -> bool:
        if obj in TYPE_MAP:
            return False

        if is_dataclass(obj):
            return True

        elif check_is_subclass(obj, Enum):
            return True

        elif is_interfice(obj):
            return True

        return False


def get_field_dependencies(typing_type: type) -> List[TypeNode]:
    if TypeNode.chk_type_node(typing_type):
        return [TypeNode(typing_type)]

    if typing.get_origin(typing_type) in [list, List]:
        args = typing.get_args(typing_type)

        if not args:
            return []
        try:
            if TypeNode.chk_type_node(args[0]):
                return [TypeNode(args[0])]
            else:
                return get_field_dependencies(args[0])

        except TypeError as e:
            raise Exception(f"Type is: {typing_type}") from e

    if typing.get_origin(typing_type) in [tuple, Tuple]:
        args = typing.get_args(typing_type)

        if not args:
            return []
        try:
            sched = []
            for arg in args:
                if TypeNode.chk_type_node(arg):
                    sched.append(TypeNode(arg))
                else:
                    sched.extend(get_field_dependencies(arg))

            return sched
        except TypeError as e:
            raise Exception(f"Type is: {typing_type}") from e

    if typing.get_origin(typing_type) in [dict, Dict]:
        args = typing.get_args(typing_type)

        if not args:
            return []
        try:
            if TypeNode.chk_type_node(args[1]):
                return [TypeNode(args[1])]
            else:
                return get_field_dependencies(args[1])

        except TypeError as e:
            raise Exception(f"Type is: {typing_type}") from e

    if typing.get_origin(typing_type) in (Union, types.UnionType):
        args = typing.get_args(typing_type)
        if not args:
            return []
        try:
            sched = []
            for arg in args:
                if TypeNode.chk_type_node(arg):
                    sched.append(TypeNode(arg))
                else:
                    sched.extend(get_field_dependencies(arg))

            return sched
        except TypeError as e:
            raise Exception(f"Type is: {typing_type}") from e

    return []


def python_value_to_typescript(v: Any) -> str:
    """
    基本值类型转换
    """
    if type(v) in VALUE_CONVERT_MAP:
        return VALUE_CONVERT_MAP[type(v)](v)

    if type(v.__class__) == enum.EnumType:
        return VALUE_CONVERT_MAP[Enum](v)

    raise UnknowFieldType(f"Unknow type {type(v)}({v})")


def python_type_to_typescript(typing_type: Any) -> str:
    """
    >>> python_type_to_typescript(str)
    'string'
    """
    if typing_type in TYPE_MAP:
        return TYPE_MAP[typing_type]

    # Nested type
    if is_dataclass(typing_type):
        return typing_type.__name__

    if check_is_subclass(typing_type, Enum):
        return typing_type.__name__

    origin = typing.get_origin(typing_type)
    if origin is typing.Literal:
        args = typing.get_args(typing_type)
        return " | ".join(map(python_value_to_typescript, args))

    if origin in (dict, Dict):
        args = typing.get_args(typing_type)
        if not args:
            return TYPE_MAP[dict]
        else:
            return "{{ [key:string]: {} }}".format(
                python_type_to_typescript(args[1]),
            )

    if origin in (Union, types.UnionType):
        args = typing.get_args(typing_type)
        return " | ".join(map(python_type_to_typescript, args))

    if origin in [list, List]:
        args = typing.get_args(typing_type)
        if not args:
            return TYPE_MAP[list]
        else:
            return "Array<{}>".format(python_type_to_typescript(args[0]))

    if origin in [tuple, Tuple]:
        args = typing.get_args(typing_type)
        if not args:
            return TYPE_MAP[tuple]
        if len(args) == 2 and args[1] is ...:
            return "Array<{}>".format(python_type_to_typescript(args[0]))
        else:
            return "[ " + ", ".join(map(python_type_to_typescript, args)) + " ]"

    if isinstance(typing_type, ForwardRef):
        return typing_type.__forward_arg__

    if isinstance(typing_type, type):
        return typing_type.__name__

    raise UnknowFieldType(f"Unknow type {typing_type}")


def field_to_typescript(field: Field) -> str:
    try:
        return python_type_to_typescript(field.type) + ";"
    except UnknowFieldType as e:
        raise UnknowFieldType(f"{field.name}: {e}")


def _traverse(node: TypeNode, visited: Dict[str, TypeNode]):
    for dep in node.get_dependencies():
        if dep.path not in visited:
            _traverse(dep, visited)

    visited[node.path] = node


def schemas2typescript(schemas: List) -> Dict[str, TypeNode]:
    """
    Args:
        schemas: A list of dataclass schemas or Enum
    interfaces.
    """
    graph: Dict[str, TypeNode] = OrderedDict()

    for schema in schemas:
        _traverse(TypeNode(schema), graph)

    return graph
